/*
 * Copyright (c) 2000-2015 Tacke Consulting, Inc
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
 * documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 
 * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions 
 * of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
 * TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
 * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 * 
 */
using System;
using System.Collections;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.IO;

namespace OpenNETCF.Media.DirectShow
{
    /// <summary>Base class</summary>
    internal class DShowGraph : IDisposable
    {
        /// <summary>Path to the input file.</summary>
        private string _input;
        /// <summary>Frequency with which the conversion status should be polled and listeners alerted.</summary>
        private int _pollFrequency = 100;

        private IMediaControl _mediaControl;
        private IMediaEvent _mediaEvent;
        private IMediaSeeking _mediaSeeking;
        private IGraphBuilder _graphBuilder;
        private IMediaPosition _mediaPosition;
        

        /// <summary>Initialize the converter.</summary>
        public DShowGraph() : this(null) { }

        /// <summary>Initialize the converter.</summary>
        /// <param name="input">The input DVR-MS file path.</param>
        public DShowGraph(string input)
        {
            _input = input;
            _dc = new DisposalCleanup();
            ConversionComplete += new GraphCompletedEventHandler(ResetWhenGraphComplete);
        }

        /// <summary>
        /// Gets or sets the frequency with which the conversion status should 
        /// be polled and listeners alerted.
        /// </summary>
        /// <remarks>A value of Timeout.Infinite (-1) will result in no polling.</remarks>
        public int PollFrequency
        {
            get { return _pollFrequency; }
            set
            {
                if (value <= 0) _pollFrequency = -1;
                _pollFrequency = value;
            }
        }

        /// <summary>Runs the conversion synchronously.</summary>
        /// <returns>The result of the conversion.</returns>
        /// <exception cref="DirectShowException">If an exception is caused by Direct Show a DirectShowException will be thrown.</exception>
        /// <exception cref="OutOfMemoryException">Will throw an OutOfMemoryException if there is a memory error during playback</exception>
        public object Run()
        {
            _cancellationPending = false;
            try
            {
                object result;
                //using (_dc = new DisposalCleanup())
                {
                    // Do the actual work
                    result = DoWork();
                }
                OnGraphComplete(null, result);
                return result;
            }
            catch (DirectShowException exc)
            {
                DirectShowPlayerControl.m_logger.WriteException("DirectShowException Exception in Run", exc);
                OnGraphComplete(exc, null);
                throw;
            }
            catch (Exception exc)
            {
                if (exc.Message == "0x8007000e")
                {
                    //Memory exception
                    DirectShowPlayerControl.m_logger.WriteException("OutOfMemoryException in Run", exc);
                    exc = new OutOfMemoryException("OutOfMemoryException during playback.", exc);
                }
                else if (exc.Message == "0x80004005")
                {
                    //E_FAIL excpetion
                    DirectShowPlayerControl.m_logger.WriteException("Unknown error (E_FAIL) during playback.", exc);
                    exc = new Exception("Unknown error (E_FAIL) during playback.", exc);
                }
                else
                {
                    DirectShowPlayerControl.m_logger.WriteException("Exception in Run", exc);
                    exc = new DirectShowException(exc);
                }

                OnGraphComplete(exc, null);
                throw exc;
            }
            catch
            {
                DirectShowPlayerControl.m_logger.WriteException("Unknown Exception in Run", new DirectShowException());
                OnGraphComplete(new DirectShowException(), null);
                throw;
            }
        }

        /// <summary>Allows for easy cleanup of COM resources.</summary>
        private DisposalCleanup _dc;

        /// <summary>Allows for easy cleanup of COM resources.</summary>
        internal DisposalCleanup DisposalCleanup { get { return _dc; } }

        /// <summary>Used to invoke the conversion process asynchronously.</summary>
        private delegate object RunHandler();

        /// <summary>Whether there is currently an asynchronous conversion running.</summary>
        private volatile bool _asyncRunning = false;

        /// <summary>Starts an asynchronous conversion.</summary>
        public void RunAsync()
        {
            if (_asyncRunning)
            {
                if (_mediaControl != null)
                    _mediaControl.Run();
                return;
            }
            _asyncRunning = true;
            ThreadPool.QueueUserWorkItem(delegate(object obj)
            {
                try
                {
                    Run();
                }
                catch
                {
                    //Trap the excetions since we are running async
                }
                //Moved this to the dispose method
                //Marshal.ReleaseComObject(_graphBuilder);
            });
        }

        /// <summary>Issues a cancellation for any currently running asynchronous operations.</summary>
        public void CancelAsync() { _cancellationPending = true; }

        /// <summary>Resets state when an asynchronous conversion completes.</summary>
        /// <param name="sender">Ignored.</param>
        /// <param name="e">Ignored.</param>
        private void ResetWhenGraphComplete(object sender, ConversionCompletedEventArgs e)
        {
            _asyncRunning = false;
        }

        /// <summary>Whether this is currently a cancellation request pending.</summary>
        private volatile bool _cancellationPending = false;

        /// <summary>Gets whether this is currently a cancellation request pending.</summary>
        protected bool CancellationPending { get { return _cancellationPending; } }

        /// <summary>Raises the ProgressChanged event.</summary>
        protected void OnProgressChanged(ulong position, ulong duration)
        {
            ProgressChangedEventHandler ev = ProgressChanged;
            if (ev != null) ev(this, new ProgressChangedEventArgs(position, duration));
        }

        /// <summary>Raises the ConversionComplete event.</summary>
        /// <param name="error">Any exception that prevented the work from completing.</param>
        /// <param name="result">The result of the conversion.</param>
        protected void OnGraphComplete(Exception error, object result)
        {
            GraphCompletedEventHandler ev = ConversionComplete;
            if (ev != null) ev(this, new ConversionCompletedEventArgs(error, result));
        }

        /// <summary>Raised when a conversion has completed..</summary>
        public event GraphCompletedEventHandler ConversionComplete;

        /// <summary>Raised when there's a progress update for a conversion.</summary>
        public event ProgressChangedEventHandler ProgressChanged;

        protected virtual object DoWork()
        {
            RunGraph(_graphBuilder);
            return null;
        }

        internal IGraphBuilder Open(DebugLogger logger)
        {
            logger.WriteLine("+DirectShowGraph.Open");

            // Get the filter graph
            object filterGraph;

            try
            {
                logger.WriteLine(" CoCreateInstance(ClassId.FilterGraph)");
                filterGraph = ClassId.CoCreateInstance(ClassId.FilterGraph);
            }
            catch (Exception ex)
            {
                throw new PlatformNotSupportedException(string.Format("Unable to create a Filtergraph (CLSID='{0})'", ClassId.FilterGraph, ex));
            }

            DisposalCleanup.Add(filterGraph);
            try
            {
                logger.WriteLine(" Cast to IGraphBuilder");
                _graphBuilder = (IGraphBuilder)filterGraph;
            }
            catch(Exception ex)
            {
                throw new PlatformNotSupportedException(string.Format("Unable to creat an IGraphBuilder instance (CLSID='{0})'", "56A868A9-0AD4-11CE-B03A-0020AF0BA770", ex));
            }

            try
            {
                logger.WriteLine(" Calling IGraphBuilder::RenderFile");
                _graphBuilder.RenderFile(_input, null);
            }
            catch (Exception ex)
            {
                throw new PlatformNotSupportedException("IGraphBuilder.RenderFile failed", ex);
            }

            logger.WriteLine("-DirectShowGraph.Open");
            return _graphBuilder;
        }

        /// <summary>Connects together two graph filters.</summary>
        /// <param name="graph">The graph on which the filters exist.</param>
        /// <param name="source">The source filter.</param>
        /// <param name="outPinName">The name of the output pin on the source filter.</param>
        /// <param name="destination">The destination filter.</param>
        /// <param name="inPinName">The name of the input pin on the destination filter.</param>
        internal void Connect(IGraphBuilder graph, IBaseFilter source, string outPinName,
            IBaseFilter destination, string inPinName)
        {
            IPin outPin = source.FindPin(outPinName);
            DisposalCleanup.Add(outPin);

            IPin inPin = destination.FindPin(inPinName);
            DisposalCleanup.Add(inPin);

            graph.Connect(outPin, inPin);
        }

        /// <summary>Runs the graph</summary>
        /// <param name="graphBuilder">The graph to be run.</param>
        internal void RunGraph(IGraphBuilder graphBuilder)
        {
            RunGraph(graphBuilder, (IBaseFilter)null);
        }

        ///// <summary>Runs the graph</summary>
        ///// <param name="graphBuilder">The graph to be run.</param>
        ///// <param name="seekableFilter">The filter to use for computing percent complete. Must implement IMediaSeeking.</param>
        //protected void RunGraph(IGraphBuilder graphBuilder, IFileSinkFilter seekableFilter)
        //{
        //    RunGraph(graphBuilder, (IBaseFilter)seekableFilter);
        //}

        /// <summary>Determines whether the specified IMediaSeeking can be used to retrieve duration and current position.</summary>
        /// <param name="seeking">The interface to check.</param>
        /// <returns>true if it can be used to retrieve duration and current position; false, otherwise.</returns>
        private static bool CanGetPositionAndDuration(IMediaSeeking seeking)
        {
            if (seeking == null) return false;
            SeekingCapabilities caps = seeking.GetCapabilities();
            if ((caps & SeekingCapabilities.CanGetDuration) != SeekingCapabilities.CanGetDuration) return false;
            if ((caps & SeekingCapabilities.CanGetCurrentPos) != SeekingCapabilities.CanGetCurrentPos) return false;
            return true;
        }

        /// <summary>Runs the graph</summary>
        /// <param name="graphBuilder">The graph to be run.</param>
        /// <param name="seekableFilter">The filter to use for computing percent complete. Must implement IMediaSeeking.</param>
        internal void RunGraph(IGraphBuilder graphBuilder, IBaseFilter seekableFilter)
        {
            // Get the necessary control and event interfaces
            _mediaControl = (IMediaControl)graphBuilder;
            _mediaEvent = (IMediaEvent)graphBuilder;

            // Get the media seeking interface to use for computing status and progress updates
            _mediaSeeking = seekableFilter as IMediaSeeking;

            if (!CanGetPositionAndDuration(_mediaSeeking))
            {
                _mediaSeeking = graphBuilder as IMediaSeeking;
                if (!CanGetPositionAndDuration(_mediaSeeking)) _mediaSeeking = null;
            }

            _mediaPosition = graphBuilder as IMediaPosition;

            // Run the graph
            _mediaControl.Run();
            ulong length = ulong.MaxValue;
            try 
            {
                if (_mediaSeeking != null)
                {
                    length = _mediaSeeking.GetDuration();
                }
            }
            catch { }

            try
            {
                if (_mediaSeeking != null)
                {
                    OnProgressChanged(_mediaSeeking.GetCurrentPosition(), length); // initial progress update stating 0% done
                }

                bool done = false;
                while (!CancellationPending && !done) // continue until we're done/cancelled
                {
                    // Poll to see how we're doing
                    EventCode statusCode = EventCode.None;
                    int hr = _mediaEvent.WaitForCompletion(PollFrequency, out statusCode);
                    switch (statusCode)
                    {
                        case EventCode.Complete:
                            done = true;
                            break;
                        case EventCode.None:
                            // Get an update on where we are with the conversion
                            if (_mediaSeeking != null)
                            {
                                ulong curPos = _mediaSeeking.GetCurrentPosition();
                                double progress = curPos * 100.0 / (double)length;
                                if (progress > 0) OnProgressChanged(curPos, length);
                            }
                            break;
                        default:
                            // Error, so throw exception
                            throw new DirectShowException(hr, null);
                    }
                }
                OnProgressChanged(length, length); // final progress update stating 100% done
            }
            finally
            {
                // We're done, so stop the graph
                try
                {
                    _mediaControl.Stop();
                }
                catch (System.Runtime.InteropServices.InvalidComObjectException)
                {
                    //Swallow this exception. This could happen if we dispose of the objects while it's still playing
                }
            }
        }

        public void Pause()
        {
            if (_mediaControl == null)
                throw new InvalidOperationException("Graph not initialized");
            _mediaControl.Pause();
        }

        public void Play()
        {
            if (_mediaControl == null)
                throw new InvalidOperationException("Graph not initialized");
            _mediaControl.Run();
        }

        public void Stop()
        {
            if (_mediaControl == null)
                throw new InvalidOperationException("Graph not initialized");
            _mediaControl.Stop();
        }

        public void StopWhenReady()
        {
            if (_mediaControl == null)
                throw new InvalidOperationException("Graph not initialized");
            _mediaControl.Pause();
            _mediaControl.StopWhenReady();
        }

        public ulong SeekTo(ulong newPos)
        {
            if (_graphBuilder == null)
                throw new InvalidOperationException("Media file not opened.");

            ulong stopTime = 0;
            ((IMediaSeeking)_graphBuilder).SetPositions(ref newPos, SeekingFlags.AbsolutePositioning, ref stopTime, SeekingFlags.NoPositioning);
            return newPos;
        }

        public IVideoWindow GetVideoWindow()
        {
            if (_graphBuilder == null)
                return null;
            return (IVideoWindow)(_graphBuilder);
        }

        internal FilterState State
        {
            get
            {
                if (_mediaControl == null)
                    return FilterState.Stopped;
                return _mediaControl.GetState(10);
            }
        }

        public ulong Position
        {
            get
            {
                if (_mediaSeeking == null)
                    return 0;
                return _mediaSeeking.GetCurrentPosition();
            }
        }

        public ulong Duration
        {
            get
            {
                if (_graphBuilder == null)
                    throw new InvalidOperationException("Media file not opened.");

                var seeking = _graphBuilder as IMediaSeeking;
                var position = _graphBuilder as IMediaPosition;

                Debug.WriteLine("Seeking: " + seeking.GetDuration().ToString());
                double d;
                position.get_Duration(out d);
                Debug.WriteLine("Position: " + d.ToString());

                return ((IMediaSeeking)_graphBuilder).GetDuration();
            }
        }

        #region IDisposable Members

        public void Dispose()
        {
            DisposalCleanup.Dispose();
            if (_graphBuilder != null)
                Marshal.ReleaseComObject(_graphBuilder);
        }

        #endregion

        internal void SetFullScreen(bool fullScreen)
        {
            if (_graphBuilder == null)
                throw new InvalidOperationException("Media file not opened.");

            IVideoWindow wnd = (IVideoWindow)GetVideoWindow();

            wnd.put_FullScreenMode(fullScreen ? 1 : 0);
        }

    }

}
